
import { globalEmitter, IEvent } from '@feng3d/event';
import { ACESFilmicToneMapping, Clock, Color, CubeRefractionMapping, CubeTextureLoader, DirectionalLight, Fog, HemisphereLight, IcosahedronGeometry, Intersection, MathUtils, Mesh, MeshLambertMaterial, MeshPhongMaterial, PerspectiveCamera, Raycaster, Scene, Sphere, Vector2, Vector3, VSMShadowMap, WebGLRenderer } from 'three';
import { OctreeHelper } from 'three/examples/jsm/helpers/OctreeHelper.js';
import { GUI, NumberController } from 'three/examples/jsm/libs/lil-gui.module.min.js';
import Stats from 'three/examples/jsm/libs/stats.module.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { Octree } from 'three/examples/jsm/math/Octree.js';
import { URDFJoint } from 'urdf-loader';
import { changeHighlightMaterial } from './changeHighlightMaterial';
import { masterRobot, robots } from './data';
import { Player } from './Player';
import { client } from '@feng3d/websocketclient';
import { PlayerInfo } from '@feng3d/websocketmessage';

export class FPSGame
{
    private clock: Clock;
    private scene: Scene;
    private camera: PerspectiveCamera;
    private renderer: WebGLRenderer;
    private stats: Stats;
    /**
     * 主角
     */
    private master: Player;
    private players: Map<string, Player> = new Map();
    private worldOctree: Octree;
    private spheres: Array<{ mesh: Mesh, collider: Sphere, velocity: Vector3 }>;
    private sphereIdx: number;
    private playerOnFloor: boolean;
    private mouseTime: number;
    private keyStates: Record<string, boolean>;
    private container: HTMLElement;

    private readonly GRAVITY = 30;
    private readonly NUM_SPHERES = 100;
    private readonly SPHERE_RADIUS = 0.2;
    private readonly STEPS_PER_FRAME = 5;
    gui = new GUI({ width: 200 });
    isMouseDown: boolean;
    mouseDownJointValue: number;
    jointInfos: { joint: URDFJoint; name: string; value: number; min: number; max: number; NumberController: NumberController; }[];
    robotControl: GUI;
    get mouseDownJoint()
    {
        return this._mouseDownJoint;
    }
    set mouseDownJoint(value: URDFJoint)
    {
        if (this._mouseDownJoint)
        {
            changeHighlightMaterial(this._mouseDownJoint, true);
        }
        this._mouseDownJoint = value;
        if (this._mouseDownJoint)
        {
            changeHighlightMaterial(this._mouseDownJoint);
        }
    }
    private _mouseDownJoint: URDFJoint;
    mouseDownClientXY: { x: number; y: number; };

    constructor()
    {
        this.clock = new Clock();
        this.sphereIdx = 0;
        this.playerOnFloor = false;
        this.mouseTime = 0;
        this.keyStates = {};
    }

    public init(containerId: string = 'container'): void
    {
        this.container = document.getElementById(containerId);
        this.initScene();
        this.initRenderer();
        this.initSpheres();
        this.initPlayer();
        this.initEventListeners();
    }

    private initScene(): void
    {

        //cubemap
        const path = 'textures/cube/SwedishRoyalCastle/';
        const format = '.jpg';
        const urls = [
            path + 'px' + format, path + 'nx' + format,
            path + 'py' + format, path + 'ny' + format,
            path + 'pz' + format, path + 'nz' + format
        ];

        const reflectionCube = new CubeTextureLoader().load(urls);
        const refractionCube = new CubeTextureLoader().load(urls);
        refractionCube.mapping = CubeRefractionMapping;

        this.scene = new Scene();
        this.scene.background = new Color(0x88ccee);
        this.scene.fog = new Fog(0x88ccee, 0, 50);
        this.scene.background = reflectionCube;

        this.camera = new PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 1000);
        this.camera.rotation.order = 'YXZ';

        const fillLight1 = new HemisphereLight(0x8dc1de, 0x00668d, 1.5);
        fillLight1.position.set(2, 1, 1);
        this.scene.add(fillLight1);

        const directionalLight = new DirectionalLight(0xffffff, 2.5);
        directionalLight.position.set(-5, 25, -1);
        directionalLight.castShadow = true;
        directionalLight.shadow.camera.near = 0.01;
        directionalLight.shadow.camera.far = 500;
        directionalLight.shadow.camera.right = 30;
        directionalLight.shadow.camera.left = -30;
        directionalLight.shadow.camera.top = 30;
        directionalLight.shadow.camera.bottom = -30;
        directionalLight.shadow.mapSize.width = 1024;
        directionalLight.shadow.mapSize.height = 1024;
        directionalLight.shadow.radius = 4;
        directionalLight.shadow.bias = -0.00006;
        this.scene.add(directionalLight);

        const loader = new GLTFLoader().setPath('./models/gltf/');
        loader.load('collision-world.glb', (gltf) =>
        {
            this.scene.add(gltf.scene);
            this.worldOctree.fromGraphNode(gltf.scene);

            gltf.scene.traverse(child =>
            {
                const mesh = child as Mesh;
                if (mesh.isMesh)
                {
                    mesh.castShadow = true;
                    mesh.receiveShadow = true;

                    const material = mesh.material as MeshPhongMaterial;
                    if (material.map)
                    {
                        material.map.anisotropy = 4;
                    }
                }
            });

            const helper = new OctreeHelper(this.worldOctree);
            helper.visible = false;
            this.scene.add(helper);

            const gui = this.gui;
            gui.add({ debug: false }, 'debug')
                .onChange((value) =>
                {
                    helper.visible = value;
                });
            this.gui = gui;
        });
    }

    private initRenderer(): void
    {
        this.renderer = new WebGLRenderer({ antialias: true });
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.renderer.setSize(window.innerWidth, window.innerHeight);
        this.renderer.setAnimationLoop(() => this.animate());
        this.renderer.shadowMap.enabled = true;
        this.renderer.shadowMap.type = VSMShadowMap;
        this.renderer.toneMapping = ACESFilmicToneMapping;
        this.container.appendChild(this.renderer.domElement);

        this.stats = new Stats();
        this.container.appendChild(this.stats.dom);
    }

    private initSpheres(): void
    {
        const sphereGeometry = new IcosahedronGeometry(this.SPHERE_RADIUS, 5);
        const sphereMaterial = new MeshLambertMaterial({ color: 0xdede8d });

        this.spheres = [];
        for (let i = 0; i < this.NUM_SPHERES; i++)
        {
            const sphere = new Mesh(sphereGeometry, sphereMaterial);
            sphere.castShadow = true;
            sphere.receiveShadow = true;
            this.scene.add(sphere);

            this.spheres.push({
                mesh: sphere,
                collider: new Sphere(new Vector3(0, -100, 0), this.SPHERE_RADIUS),
                velocity: new Vector3()
            });
        }
    }

    private initPlayer(): void
    {
        this.worldOctree = new Octree();
        this.master = new Player(this.scene, masterRobot.name);
        this.master.name = client.username;
        this.players.set(client.username, this.master);
        this.master.onload = () =>
        {
            this.onMasterLoaded();
        };
        //
        this.gui.add(masterRobot, 'name').options(Object.keys(robots)).onChange(() =>
        {
            this.master.robotName = masterRobot.name;
        });

        this.gui.add({
            savePose: () =>
            {
                this.master.savePose();
            }
        }, "savePose").name("Save Pose")

        this.gui.add({
            clearSavedPoses: () =>
            {
                this.master.clearSavedPoses();
            }
        }, "clearSavedPoses").name("Clear Saved Poses")

        // 监听添加玩家事件
        globalEmitter.on("addPlayerToScene", this.onAddPlayerToScene, this);
        globalEmitter.on("removePlayerFromScene", this.onRemovePlayerFromScene, this);
    }

    private onMasterLoaded(): void
    {
        if (this.robotControl)
        {
            this.robotControl.destroy();
            this.robotControl = null;
        }
        const robotControl = this.gui.addFolder('Robot Control');
        this.robotControl = robotControl;
        const robot = this.master.robot;
        const jointInfos: {
            joint: URDFJoint;
            name: string;
            value: number;
            min: number;
            max: number;
            NumberController: NumberController,
        }[] = [];
        for (const key in robot.joints)
        {
            const joint = robot.joints[key];

            const jointInfo = {
                joint,
                name: key,
                value: joint.angle as number,
                min: MathUtils.radToDeg(joint.limit.lower as any),
                max: MathUtils.radToDeg(joint.limit.upper as any),
                NumberController: null as any
            };

            if (jointInfo.value !== undefined)
            {
                jointInfo.NumberController = robotControl.add(jointInfo, "value", jointInfo.min, jointInfo.max, 1)
                    .name(joint.name)
                    .onChange(() =>
                    {
                        joint.setJointValue(MathUtils.degToRad(jointInfo.value as any));
                    });


                jointInfos.push(jointInfo);
            }
        }
        robotControl.add({
            reset: () =>
            {
                for (let i = 0; i < jointInfos.length; i++)
                {
                    jointInfos[i].NumberController.setValue(0);
                }
            }
        }, 'reset').name('Reset');

        this.jointInfos = jointInfos;
    }

    private updateJointInfos()
    {
        if (!this.jointInfos) return;
        for (let i = 0; i < this.jointInfos.length; i++)
        {
            const jointInfo = this.jointInfos[i];
            jointInfo.NumberController.setValue(MathUtils.radToDeg(jointInfo.joint.angle as number));
        }
    }

    private onAddPlayerToScene(event: IEvent<PlayerInfo>): void
    {
        const playerInfo = event.data;
        let player = this.players.get(playerInfo.name)
        if (!player)
        {
            player = new Player(this.scene);
            this.players.set(playerInfo.name, player);
        }
        player.onUpdateData(playerInfo);
    }

    private onRemovePlayerFromScene(event: IEvent<string>)
    {
        const playerName = event.data;
        const player = this.players.get(playerName);
        if (player)
        {
            player.remove();
            this.players.delete(playerName);
        }
    }

    private initEventListeners(): void
    {
        document.addEventListener('keydown', (event) =>
        {
            this.keyStates[event.code] = true;
        });

        document.addEventListener('keyup', (event) =>
        {
            this.keyStates[event.code] = false;
        });

        this.container.addEventListener('mousedown', (event) =>
        {
            this.mouseDownClientXY = { x: event.clientX, y: event.clientY };
            event.clientX
            if (this.master?.robot)
            {
                const intersects: Intersection<Mesh>[] = [];
                const mouse = new Vector2(
                    (event.clientX / window.innerWidth) * 2 - 1,
                    - (event.clientY / window.innerHeight) * 2 + 1
                );
                raycaster.setFromCamera(mouse, this.camera);
                raycaster.intersectObject(this.master.robot, true, intersects);

                if (intersects.length > 0)
                {
                    let object = intersects[0].object;

                    let joint: URDFJoint;
                    while (object)
                    {
                        if ((object as any as URDFJoint).type === "URDFJoint")
                        {
                            joint = object as any as URDFJoint;
                            break;
                        }
                        object = object.parent as any;
                    }
                    if (joint)
                    {
                        this.mouseDownJointValue = MathUtils.radToDeg(joint.angle as number);
                        this.mouseDownJoint = joint;
                    }
                }
            }

            //
            this.isMouseDown = true;
            this.mouseTime = performance.now();
        });

        document.addEventListener("dblclick", (e) =>
        {
            this.throwBall();
        });

        document.addEventListener('mouseup', () =>
        {
            this.mouseDownJointValue = undefined;
            this.mouseDownJoint = undefined;

            this.isMouseDown = false;
        });

        document.body.addEventListener('mousemove', (event) =>
        {
            if (this.mouseDownJoint)
            {
                const offset = event.clientX - this.mouseDownClientXY.x + event.clientY - this.mouseDownClientXY.y;

                const newValue = this.mouseDownJointValue + offset * 1;
                this.mouseDownJoint.setJointValue(MathUtils.degToRad(newValue));
                //
                this.updateJointInfos()
                this.master.jointInfosChanged()

                return;
            }
            if (!this.isMouseDown) return;
            this.camera.rotation.y -= event.movementX / 500;
            this.camera.rotation.x -= event.movementY / 500;
        });

        window.addEventListener('resize', () => this.onWindowResize());
    }

    private onWindowResize(): void
    {
        this.camera.aspect = window.innerWidth / window.innerHeight;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(window.innerWidth, window.innerHeight);
    }

    private throwBall(): void
    {
        const sphere = this.spheres[this.sphereIdx];
        this.camera.getWorldDirection(this.master.direction);

        sphere.collider.center.copy(this.master.collider.end)
            .addScaledVector(this.master.direction, this.master.collider.radius * 1.5);

        const impulse = 15 + 30 * (1 - Math.exp((this.mouseTime - performance.now()) * 0.001));
        sphere.velocity.copy(this.master.direction).multiplyScalar(impulse);
        sphere.velocity.addScaledVector(this.master.velocity, 2);

        this.sphereIdx = (this.sphereIdx + 1) % this.spheres.length;
    }

    private playerCollisions(): void
    {
        const result = this.worldOctree.capsuleIntersect(this.master.collider);
        this.playerOnFloor = false;

        if (result)
        {
            this.playerOnFloor = result.normal.y > 0;

            if (!this.playerOnFloor)
            {
                this.master.velocity.addScaledVector(result.normal, -result.normal.dot(this.master.velocity));
            }

            if (result.depth >= 1e-10)
            {
                this.master.collider.translate(result.normal.multiplyScalar(result.depth));
            }
        }
    }

    private updatePlayer(deltaTime: number): void
    {
        let damping = Math.exp(-4 * deltaTime) - 1;

        if (!this.playerOnFloor)
        {
            this.master.velocity.y -= this.GRAVITY * deltaTime;
            damping *= 0.1;
        }

        this.master.velocity.addScaledVector(this.master.velocity, damping);

        const deltaPosition = this.master.velocity.clone().multiplyScalar(deltaTime);
        this.master.collider.translate(deltaPosition);

        this.playerCollisions();

        let newPosition = new Vector3().addVectors(this.master.collider.start, this.master.collider.end).multiplyScalar(0.5);
        newPosition = newPosition.sub(this.camera.getWorldDirection(new Vector3).multiplyScalar(2));

        this.camera.position.copy(newPosition);

    }

    private playerSphereCollision(sphere: { collider: Sphere, velocity: Vector3 }): void
    {
        const center = new Vector3().addVectors(this.master.collider.start, this.master.collider.end).multiplyScalar(0.5);
        const sphere_center = sphere.collider.center;
        const r = this.master.collider.radius + sphere.collider.radius;
        const r2 = r * r;

        for (const point of [this.master.collider.start, this.master.collider.end, center])
        {
            const d2 = point.distanceToSquared(sphere_center);

            if (d2 < r2)
            {
                const normal = new Vector3().subVectors(point, sphere_center).normalize();
                const v1 = new Vector3().copy(normal).multiplyScalar(normal.dot(this.master.velocity));
                const v2 = new Vector3().copy(normal).multiplyScalar(normal.dot(sphere.velocity));

                this.master.velocity.add(v2).sub(v1);
                sphere.velocity.add(v1).sub(v2);

                const d = (r - Math.sqrt(d2)) / 2;
                sphere_center.addScaledVector(normal, -d);
            }
        }
    }

    private spheresCollisions(): void
    {
        for (let i = 0, length = this.spheres.length; i < length; i++)
        {
            const s1 = this.spheres[i];

            for (let j = i + 1; j < length; j++)
            {
                const s2 = this.spheres[j];

                const d2 = s1.collider.center.distanceToSquared(s2.collider.center);
                const r = s1.collider.radius + s2.collider.radius;
                const r2 = r * r;

                if (d2 < r2)
                {
                    const normal = new Vector3().subVectors(s1.collider.center, s2.collider.center).normalize();
                    const v1 = new Vector3().copy(normal).multiplyScalar(normal.dot(s1.velocity));
                    const v2 = new Vector3().copy(normal).multiplyScalar(normal.dot(s2.velocity));

                    s1.velocity.add(v2).sub(v1);
                    s2.velocity.add(v1).sub(v2);

                    const d = (r - Math.sqrt(d2)) / 2;
                    s1.collider.center.addScaledVector(normal, d);
                    s2.collider.center.addScaledVector(normal, -d);
                }
            }
        }
    }

    private updateSpheres(deltaTime: number): void
    {
        this.spheres.forEach(sphere =>
        {
            sphere.collider.center.addScaledVector(sphere.velocity, deltaTime);

            const result = this.worldOctree.sphereIntersect(sphere.collider);

            if (result)
            {
                sphere.velocity.addScaledVector(result.normal, -result.normal.dot(sphere.velocity) * 1.5);
                sphere.collider.center.add(result.normal.multiplyScalar(result.depth));
            } else
            {
                sphere.velocity.y -= this.GRAVITY * deltaTime;
            }

            const damping = Math.exp(-1.5 * deltaTime) - 1;
            sphere.velocity.addScaledVector(sphere.velocity, damping);

            this.playerSphereCollision(sphere);
        });

        this.spheresCollisions();

        for (const sphere of this.spheres)
        {
            sphere.mesh.position.copy(sphere.collider.center);
        }
    }

    private getForwardVector(): Vector3
    {
        this.camera.getWorldDirection(this.master.direction);
        this.master.direction.y = 0;
        this.master.direction.normalize();
        return this.master.direction;
    }

    private getSideVector(): Vector3
    {
        this.camera.getWorldDirection(this.master.direction);
        this.master.direction.y = 0;
        this.master.direction.normalize();
        this.master.direction.cross(this.camera.up);
        return this.master.direction;
    }

    private teleportPlayerIfOob(): void
    {
        if (this.camera.position.y <= -25)
        {
            this.master.collider.start.set(0, 0.35, 0);
            this.master.collider.end.set(0, 1, 0);
            this.master.collider.radius = 0.35;
            this.camera.position.copy(this.master.collider.end);
            this.camera.rotation.set(0, 0, 0);
        }
    }

    private controls(deltaTime: number): void
    {
        const speedDelta = deltaTime * (this.playerOnFloor ? 25 : 8);

        if (this.keyStates['KeyW'])
        {
            this.master.velocity.add(this.getForwardVector().multiplyScalar(speedDelta));
        }

        if (this.keyStates['KeyS'])
        {
            this.master.velocity.add(this.getForwardVector().multiplyScalar(-speedDelta));
        }

        if (this.keyStates['KeyA'])
        {
            this.master.velocity.add(this.getSideVector().multiplyScalar(-speedDelta));
        }

        if (this.keyStates['KeyD'])
        {
            this.master.velocity.add(this.getSideVector().multiplyScalar(speedDelta));
        }

        if (this.playerOnFloor && this.keyStates['Space'])
        {
            this.master.velocity.y = 15;
        }
    }

    private animate(): void
    {
        const deltaTime = Math.min(0.05, this.clock.getDelta()) / this.STEPS_PER_FRAME;

        for (let i = 0; i < this.STEPS_PER_FRAME; i++)
        {
            this.controls(deltaTime);
            this.updatePlayer(deltaTime);
            this.updateSpheres(deltaTime);
            this.teleportPlayerIfOob();
        }

        this.renderer.render(this.scene, this.camera);
        this.stats.update();
    }
}

const raycaster = new Raycaster();
